<?php
/**
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or 
 * substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 * @license     http://www.opensource.org/licenses/mit-license.php MIT License
 * @copyright   Copyright (c) 2009 James Rodenkirch, Active Binary
 * @author      James Rodenkirch <james@activebinary.com>
 * @package     ActiveText
 * @version     $Rev: 9 $
 */

/* ============================================================================================== */

/**
 * Custom exception to allow catching of ActiveText warnings before the filter up to the global
 * error handler
 * 
 * @package     ActiveText
 * @author      James Rodenkirch <james@activebinary.com>
 */
class ActiveTextException extends Exception { }

/* ============================================================================================== */

/**
 * ActiveText by Active Binary
 * Dynamic text based image creation
 * 
 * @package     ActiveText
 * @author      James Rodenkirch <james@activebinary.com>
 */
class ActiveText
{
  /**
   * @var array configuration data parsed from the config.ini file
   */
  private $_config = array();
  
  /**
   * @var string hexidecimal value for the backround color
   */
  private $_backColor = null;
  
  /**
   * @var string hexidecimal value for the font color
   */
  private $_color = null;
  
  /**
   * @var string file name for the desired ttf font to use while rendering
   */
  private $_font = null;
  
  /**
   * @var string name of the preset set in the config.ini file to use
   */
  private $_preset = null;
  
  /**
   * @var string size of the font (in pt) to use use while rendering
   */
  private $_size = null;
  
  /**
   * @var bool flag to controll background transparency
   */
  private $_transparent = null;
  
  /**
   * @var string text to render as an image
   */
  private $_text = null;
  
  /* -------------------------------------------------------------------------------------------- */
  
  /**
   * class constructor
   * @param $config path to the configuration file
   */
  private function __construct($config)
  {
    // make sure the passed config exists
    if (!is_file($config)){
      throw new ActiveTextException('The passed config file does not exist: '. $config);
    }
    
    // mkae sure we can output the image in the requested format
    $gd = gd_info();
    if ($this->config['output'] == 'gif' && $gd['GIF Create Support'] === false){
      throw new ActiveTextException('GD gif support is not available');
    }
    if ($this->config['output'] == 'png' && $gd['PNG Support'] === false){
      throw new ActiveTextException('GD gif support is not available');
    }
    if ($this->config['output'] == 'jpg' && $gd['JPG Support'] === false){
      throw new ActiveTextException('GD gif support is not available');
    }
    
    // parse the config and store in the object instance
    $this->_config = parse_ini_file($config, true);
  }
  
  /**
   * factory method to kick off object creation
   * @param $config path to the configuration file
   * @return ActiveText image generation object
   */
  public static function create($config=null)
  {
    // make sure a config was passed
    if (is_null($config)) {
      throw new ActiveTextException(
        'You must pass a path to your config file to '. __CLASS__ . '->' . __METHOD__
      );
    }
    
    // make sure we have GD support
    if (!function_exists('gd_info')) {
      throw new ActiveTextException('GD image functions are not available');
    }
    
    // make sure we have TTF support
    if (!function_exists('imagettftext')) {
      throw new ActiveTextException('GD TrueType functions are not available');
    }
    
    // return a reference to a new object
    return new ActiveText($config);
  }
  
  /* -------------------------------------------------------------------------------------------- */
  
  /**
   * method to set the background color. value can be either full or shorthand hex
   * @param $value hexideciamal value for the requested color
   */
  public function setBackColor($value)
  {
    $this->_cleanse($value, __METHOD__);
    $this->_backColor = $value;
    
    return $this;
  }
  
  /**
   * method to set the font color. value can be either full or shorthand hex
   * @param $value hexideciamal value for the requested color
   */
  public function setColor($value)
  {
    $this->_cleanse($value, __METHOD__);
    $this->_color = $value;
    
    return $this;
  }
  
  /**
   * method to set the font file to use for generation
   * @param $value font file name {name}.ttf
   */
  public function setFont($value)
  {
    $this->_cleanse($value, __METHOD__);
    $this->_font = $value;
    
    return $this;
  }
  
  /**
   * method to set values based on the passed preset. the method will grab all the configuarion data
   * fro mthe config file and save into the instance as needed
   * @param $value preset name set in the config file
   */
  public function setPreset($value)
  {
    // make sure the desired preset is defined in the config
    if (!isset($this->_config[$value])){
      throw new ActiveTextException("The desired preset '{$value}' is not defined in the config");
    }
    
    // loop through the settings in the config and save into the instance
    foreach($this->_config[$value] as $s=>$v){
      switch($s)
      {
        case 'backColor';
          $this->setBackColor($v); break;
        case 'color';
          $this->setColor($v); break;
        case 'font';
          $this->setFont($v); break;
        case 'size';
          $this->setSize($v); break;
        case 'text';
          $this->setText($v); break;
        case 'transparent';
          $this->setTransparency($v); break;
      }
    }
    
    return $this;
  }
  
  /**
   * method to set the font size
   * @param $value size (in pt) to use when rendering the text. {size}pt
   */
  public function setSize($value)
  {
    $this->_cleanse($value, __METHOD__);
    $this->_size = $value;
    
    return $this;
  }
  
  /**
   * method to set the text to render as an image
   * @param $value text to render
   */
  public function setText($value)
  {
    $this->_cleanse($value, __METHOD__);
    $this->_text = $value;
    
    return $this;
  }
  
  /**
   * method to set the transparency flag
   * @param $value boolean value for transparency flag
   */
  public function setTransparency($value)
  {
    $this->_cleanse($value, __METHOD__);
    $this->_transparent = $value;
    
    return $this;
  }
  
  /**
   * method to cleanse the passed values
   * @param $value piece of data to cleanse
   * @param $caller method that requested the cleansing
   */
  private function _cleanse(&$value, $caller)
  {
    switch ($caller){
      // make sure colors are proper hex, and convert 3 char shorthand to 6 char
      case 'setBackColor':
      case 'setColor':
        $value = preg_replace("|^a-f0-9|i","",$value);
        if (strlen($value) != 3 && strlen() != 6){
          throw new ActiveTextException("Color hex value needs to be 3 or 6 characters");
        }
        if (strlen($value) == 3) {
          $value = "{$value[0]}{$value[0]}{$value[1]}{$value[1]}{$value[2]}{$value[2]}";
        }
        break;
          
      // make sure numeric values are set properly
      case 'setSize':
        $value = preg_replace("|^0-9\.\-|","",$value);
        break;
          
      // make sure the passed font does not have special characters in it
      case 'setFont':
        $value = preg_replace("|^a-z0-9\.\_|i","",$value);
        break;
        
      // url decode the passed text
      case 'setText':
        $value = urldecode($value);
        break;
    }
    
    return $this;
  }
  
  /* -------------------------------------------------------------------------------------------- */
  
  /**
   * method to generate the image
   */
  public function output()
  {
    // look for a cached version and use it if it exists
    $this->_checkCache();
    
    // build the path to the font file
    $font = $this->_config['paths']['font'] . "/" . $this->_font;
    
    // find the dimensions of the text being created
    list($height, $width, $offset) = $this->_calculateDimensions($font);
    
    // create an image with the dimensions of the text to be generated
    $im = imagecreatetruecolor($width, $height);
    
    // set colors
    $txt = imagecolorallocate(
      $im, hexdec(
        substr($this->_color,0,2)), 
        hexdec(substr($this->_color,2,2)), 
        hexdec(substr($this->_color,4,2)
      )
    );
    $bg = imagecolorallocate(
      $im, hexdec(
        substr($this->_backColor,0,2)), 
        hexdec(substr($this->_backColor,2,2)), 
        hexdec(substr($this->_backColor,4,2)
      )
    );
    
    // make background transparent if requested
    if ($this->_transparent) {
      imagecolortransparent($im, $bg);
    }
    
    // fill the image with the background color
    imagefilledrectangle($im, 0, 0, $width, $height, $bg);
    
    // write the text to the image
    imagettftext($im, $this->_size, 0, 0, $offset, $txt, $font, $this->_text);
    
    // generate image based on desired output type
    ob_start();
    
      switch ($this->_config['output']) {
        case 'png':
          header('Content-type: image/png');
          imagepng($im);
          break;
        case 'gif':
          header('Content-type: image/gif');
          imagegif($im);
          break;
        case 'jpg':
          header('Content-type: image/jpg');
          imagejpeg($im);
          break;
      }
      
    $buffer = ob_get_clean();
    imagedestroy($im);
    
    // save to the cache
    $this->_saveCache($buffer);
    
    // display the image
    echo $buffer;
  }
  
  /**
   * method to check the cache for an existing version of the requested image
   */
  private function _checkCache()
  {
    // get a string representation of the object
    ob_start();
    var_dump(get_object_vars($this));
    $buffer = ob_get_clean();
    
    // build the hash for this object
    $hash = md5($buffer);
    
    // check if the cache file exists, if it does use it
    if (file_exists("{$this->_config['paths']['cache']}/{$hash}")){
      die(file_get_contents("{$this->_config['paths']['cache']}/{$hash}"));
    }
  }
  
  /**
   * method to save the generated image to the cache
   * @param $buffer generated contents of the iamge file
   */
  private function _saveCache($buffer)
  {
    // build the hash for this object
    $hash = md5($buffer);
    
    // save to the cache
    file_put_contents("{$this->_config['paths']['cache']}/{$hash}", $buffer);
  }
  
  /**
   * method to calculate the dimensions of the image based on the text being rendered
   * @param $font path to the font file to use
   * @return array $height, $width, $offset
   */
  private function _calculateDimensions($font)
  {
    // find the dimensions of the text being created
    $box = imagettfbbox($this->_size, 0, $font, $this->_text);
    $height = $box[7] - $box[1];  // upper left y - lower left y
    $width = $box[2] - $box[0];   // lower right x - lower left x
    
    // make sure we have positive numbers
    if ($height<0) $height *= -1;
    if ($width<0) $width *= -1;
    
    // set the offset
    $offset = $height - $box[1];
    
    // return array of data
    return array($height, $width, $offset);
  }
}
